﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Xml;
using AvalonDock;
using FxArchitect.Xna_3_1;
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Folding;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Highlighting.Xshd;
using Microsoft.Win32;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using SlimDX;
using SlimDX.D3DCompiler;

namespace FxArchitect
{
    public class FxInfo : DocumentContent
    {
        static List<FxInfo> ms_registery = new List<FxInfo>();

        class FxIncludeHelper : Include
        {
            FxInfo fxInfo;

            public FxIncludeHelper(FxInfo fxInfo)
            {
                this.fxInfo = fxInfo;
            }

            #region Include Members

            void Include.Close(Stream stream)
            {
                if (stream != null)
                    stream.Close();
            }

            void Include.Open(IncludeType type, string fileName, Stream parentStream, out Stream stream)
            {
                FileInfo fi = new FileInfo(fxInfo.SourceFilename);
                string fullPath = Path.GetFullPath(Path.Combine(fi.Directory.FullName, fileName));

                // need to find if the file has already been loaded
                FxInfo fxInclude = null;
                for (int i = 0; i < fxInfo.FxIncludes.Count; i++)
                {
                    if (string.Equals(fxInfo.FxIncludes[i].SourceFilename, fullPath,
                                      StringComparison.CurrentCultureIgnoreCase))
                    {
                        fxInclude = fxInfo.FxIncludes[i];
                        break;

                    }
                }

                // not currently loaded, read it in
                if (fxInclude == null)
                    fxInclude = fxInfo.AddInclude(fullPath, fileName);

                string code = fxInclude.Editor.Document.Text;
                stream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(code));
            }

            #endregion
        }

        #region Fields

        static uint nextAutoNameIndex = 0;

        GraphicsResourceManager graphicsManager;

        FxInfo fxParent;
        List<FxInfo> fxIncludes;
        string sourceFilename, hlslIncludePath;

        ComboBox cbTechnique;
        TreeViewItem treeViewRoot, treeViewIncludes;
        TextEditor textEditor;
        FoldingManager foldingManager;
        AbstractFoldingStrategy foldingStrategy;

        FxIncludeHelper includeHelper;
        ShaderMacro[] preprocessorMacros;
        List<XEffectErrorOrWarning> errorsAndWarnings;
        int instructionCount = 0;

        bool hasChangedSinceLastSave;

        #endregion

        #region Properties

        /// <summary>
        /// If this instance represents an included file, this will point to the instance
        /// which included it.
        /// </summary>
        public FxInfo FxParent { get { return fxParent; } }
        /// <summary>
        /// A list of files which were included in this instance.
        /// </summary>
        public List<FxInfo> FxIncludes { get { return fxIncludes; } }
        /// <summary>
        /// The text editor used to manipulate the shader code contained in this instance.
        /// </summary>
        public TextEditor Editor { get { return textEditor; } }
        /// <summary>
        /// The tree view node which sits as the root of this instance.
        /// </summary>
        public TreeViewItem TreeViewRoot { get { return treeViewRoot; } }
        /// <summary>
        /// A list of errors and warnings that were generated during compilation.
        /// </summary>
        public List<XEffectErrorOrWarning> Errors { get { return errorsAndWarnings; } }
        /// <summary>
        /// Full path to the file this instance was loaded from.
        /// </summary>
        public string SourceFilename { get { return sourceFilename; } }
        /// <summary>
        /// Check if this instance is an effect file.
        /// </summary>
        public bool IsEffect { get { return (fxParent == null); } }
        /// <summary>
        /// Check if this instance is an include file.
        /// </summary>
        public bool IsInclude { get { return (fxParent != null); } }

        #endregion

        #region Events

        /// <summary>
        /// Raised when a new error or warning is generated.
        /// </summary>
        public event Action<XEffectErrorOrWarning> RaiseError = delegate { };
        /// <summary>
        /// Raised when an existing error or warning is canceled.
        /// </summary>
        public event Action<XEffectErrorOrWarning> RemoveError = delegate { };
        /// <summary>
        /// Raised when this instance is actually being closed, not when the document editor for it
        /// is being closed.
        /// </summary>
        public event Action<FxInfo> Closing = delegate { };

        #endregion

        #region Initialization

        /// <summary>
        /// Create a new effect or effect include file.
        /// </summary>
        /// <param name="filename">Path to the file to load, if this is not present then
        /// a default effect will be loaded.</param>
        public FxInfo(Game game, string filename) : this(game, filename, null) { }
        protected FxInfo(Game game, string filename, FxInfo parent)
        {
            this.fxParent = parent;
            fxIncludes = new List<FxInfo>();
            errorsAndWarnings = new List<XEffectErrorOrWarning>();

            if (IsEffect)
            {
                graphicsManager = new GraphicsResourceManager(game);
                try { graphicsManager.Initialize(); }
                catch { }
                graphicsManager.EffectCreated += new Action<Microsoft.Xna.Framework.Graphics.Effect>(graphicsManager_EffectCreated);
            }

            bool useDefaultFx = false;
            if (!string.IsNullOrWhiteSpace(filename))
                sourceFilename = Path.GetFullPath(filename);
            else
            {
                useDefaultFx = true;
                sourceFilename = Path.GetFullPath(string.Format("NewEffect{0}.fx",
                                                                nextAutoNameIndex++));
            }

            InitializeTextEditor();
            if (IsEffect)
            {
                InitializeCompiler();
                InitializeTreeViewItem();
            }

            Stream stream = null;
            try
            {
                if (useDefaultFx && IsEffect)
                    stream = new MemoryStream(FxResources.DefaultShader);
                else if (!useDefaultFx && File.Exists(sourceFilename))
                    stream = new FileStream(sourceFilename, FileMode.Open);

                if (stream != null)
                {
                    using (StreamReader reader = new StreamReader(stream))
                    {
                        textEditor.Document.Text = reader.ReadToEnd();
                    }
                }
                else
                    textEditor.Document.Text = string.Empty;
            }
            finally
            {
                if (stream != null)
                {
                    stream.Close();
                    stream.Dispose();
                }
            }

            Title = Path.GetFileName(sourceFilename);
            Content = textEditor;

            ms_registery.Add(this);
        }

        void InitializeTextEditor()
        {
            // initialize the text editor with default values
            textEditor = new TextEditor();
            textEditor.Foreground = Brushes.White;
            textEditor.Background = new SolidColorBrush(new System.Windows.Media.Color() { R = 0x20, G = 0x20, B = 0x20, A = 0xFF });
            textEditor.HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch;
            textEditor.VerticalAlignment = System.Windows.VerticalAlignment.Stretch;
            textEditor.WordWrap = TextEditorSettings.Default.WordWrap;
            textEditor.ShowLineNumbers = TextEditorSettings.Default.LineNumbers;
            textEditor.TextChanged += new EventHandler(textEditor_TextChanged);
            textEditor.TextArea.TextEntered += new TextCompositionEventHandler(TextArea_TextEntered);

            // read in the syntax highlighting ruleset
            using (Stream stream = new MemoryStream(FxResources.hlsl_Direct3D9))
            {
                using (XmlReader reader = new XmlTextReader(stream))
                {
                    textEditor.SyntaxHighlighting = HighlightingLoader.Load(reader, HighlightingManager.Instance);
                }
            }

            // install the HLSL folding manager
            foldingManager = FoldingManager.Install(textEditor.TextArea);
            foldingStrategy = new HLSLFoldingStrategy();
        }
        void InitializeCompiler()
        {
            // include files can not be compiled alone
            if (IsInclude) return;

            preprocessorMacros = new ShaderMacro[]
            {
                new ShaderMacro("WINDOWS", string.Empty),
                new ShaderMacro("FX_ARCHITECT", string.Empty)
            };

            includeHelper = new FxIncludeHelper(this);
        }
        void InitializeTreeViewItem()
        {
            #region Root

            ContextMenu ctxRoot = new System.Windows.Controls.ContextMenu();

            MenuItem ctxClose = new MenuItem();
            ctxClose.Header = "Close";
            ctxClose.Click += new RoutedEventHandler(ctxClose_Click);
            ctxRoot.Items.Add(ctxClose);

            treeViewRoot = new TreeViewItem();
            treeViewRoot.HorizontalAlignment = HorizontalAlignment.Stretch;
            treeViewRoot.Header = Path.GetFileName(sourceFilename);
            treeViewRoot.ContextMenu = ctxRoot;

            #endregion

            #region Include Files

            ContextMenu ctxInclude = new ContextMenu();

            MenuItem ctxIncludeAddNew = new MenuItem();
            ctxIncludeAddNew.Header = "Add new file";
            ctxIncludeAddNew.Click += new RoutedEventHandler(ctxIncludeAddNew_Click);
            ctxInclude.Items.Add(ctxIncludeAddNew);
            
            MenuItem ctxIncludeAddExisting = new MenuItem();
            ctxIncludeAddExisting.Header = "Add existing file";
            ctxIncludeAddExisting.Click += new RoutedEventHandler(ctxIncludeAddExisting_Click);
            ctxInclude.Items.Add(ctxIncludeAddExisting);

            MenuItem ctxIncludeRemoveAll = new MenuItem();
            ctxIncludeRemoveAll.Header = "Remove all files";
            ctxIncludeRemoveAll.Click += new RoutedEventHandler(ctxIncludeRemoveAll_Click);
            ctxInclude.Items.Add(ctxIncludeRemoveAll);

            treeViewIncludes = new TreeViewItem();
            treeViewIncludes.HorizontalAlignment = HorizontalAlignment.Stretch;
            treeViewIncludes.Header = "Included Files";
            treeViewIncludes.ContextMenu = ctxInclude;
            treeViewRoot.Items.Add(treeViewIncludes);

            #endregion

            #region Render States

            ComboBox cbSamplerIndex = new ComboBox();

            cbTechnique = new ComboBox();
            cbTechnique.HorizontalAlignment = HorizontalAlignment.Stretch;
            cbTechnique.SelectionChanged += (sender, e) =>
            {
                graphicsManager.ActiveTechniqueName = cbTechnique.SelectedItem as string;
            };

            ComboBox cbMinFilter = new ComboBox();
            cbMinFilter.HorizontalAlignment = HorizontalAlignment.Stretch;
            cbMinFilter.SelectionChanged += (sender, e) =>
            {
                SamplerStateBlock state = graphicsManager.GetSamplerStatePS(cbSamplerIndex.SelectedIndex);
                state.MinFilter = (Microsoft.Xna.Framework.Graphics.TextureFilter)(cbMinFilter.SelectedIndex + (cbMinFilter.SelectedIndex > 3 ? 2 : 0));
                graphicsManager.SetSamplerStatePS(state, cbSamplerIndex.SelectedIndex);
            };

            ComboBox cbMagFilter = new ComboBox();
            cbMagFilter.HorizontalAlignment = HorizontalAlignment.Stretch;
            cbMagFilter.SelectionChanged += (sender, e) =>
            {
                SamplerStateBlock state = graphicsManager.GetSamplerStatePS(cbSamplerIndex.SelectedIndex);
                state.MagFilter = (Microsoft.Xna.Framework.Graphics.TextureFilter)(cbMagFilter.SelectedIndex + (cbMagFilter.SelectedIndex > 3 ? 2 : 0));
                graphicsManager.SetSamplerStatePS(state, cbSamplerIndex.SelectedIndex);
            };

            ComboBox cbMipFilter = new ComboBox();
            cbMipFilter.HorizontalAlignment = HorizontalAlignment.Stretch;
            cbMipFilter.SelectionChanged += (sender, e) =>
            {
                SamplerStateBlock state = graphicsManager.GetSamplerStatePS(cbSamplerIndex.SelectedIndex);
                state.MipFilter = (Microsoft.Xna.Framework.Graphics.TextureFilter)(cbMipFilter.SelectedIndex + (cbMipFilter.SelectedIndex > 3 ? 2 : 0));
                graphicsManager.SetSamplerStatePS(state, cbSamplerIndex.SelectedIndex);
            };

            ComboBox cbAddressU = new ComboBox();
            cbAddressU.HorizontalAlignment = HorizontalAlignment.Stretch;
            cbAddressU.SelectionChanged += (sender, e) =>
            {
                SamplerStateBlock state = graphicsManager.GetSamplerStatePS(cbSamplerIndex.SelectedIndex);
                state.AddressU = (TextureAddressMode)(cbAddressU.SelectedIndex + 1);
                graphicsManager.SetSamplerStatePS(state, cbSamplerIndex.SelectedIndex);
            };

            ComboBox cbAddressV = new ComboBox();
            cbAddressV.HorizontalAlignment = HorizontalAlignment.Stretch;
            cbAddressV.SelectionChanged += (sender, e) =>
            {
                SamplerStateBlock state = graphicsManager.GetSamplerStatePS(cbSamplerIndex.SelectedIndex);
                state.AddressV = (TextureAddressMode)(cbAddressV.SelectedIndex + 1);
                graphicsManager.SetSamplerStatePS(state, cbSamplerIndex.SelectedIndex);
            };

            ComboBox cbAddressW = new ComboBox();
            cbAddressW.HorizontalAlignment = HorizontalAlignment.Stretch;
            cbAddressW.SelectionChanged += (sender, e) =>
            {
                SamplerStateBlock state = graphicsManager.GetSamplerStatePS(cbSamplerIndex.SelectedIndex);
                state.AddressW = (TextureAddressMode)(cbAddressW.SelectedIndex + 1);
                graphicsManager.SetSamplerStatePS(state, cbSamplerIndex.SelectedIndex);
            };

            Button btnTexture = new Button();
            btnTexture.HorizontalAlignment = HorizontalAlignment.Stretch;
            btnTexture.Click += (sender, e) =>
            {
                OpenFileDialog openDialog = new OpenFileDialog();
                openDialog.CheckFileExists = true;
                openDialog.Filter = FxResources.Filter_TextureFile;

                if (openDialog.ShowDialog() ?? false)
                {
                    graphicsManager.LoadTexture(openDialog.FileName, cbSamplerIndex.SelectedIndex);
                    btnTexture.Content = openDialog.FileName;
                }
            };

            Array texFilterValues = Enum.GetValues(typeof(Microsoft.Xna.Framework.Graphics.TextureFilter));
            foreach (object texFilter in texFilterValues)
            {
                cbMagFilter.Items.Add(texFilter);
                cbMinFilter.Items.Add(texFilter);
                cbMipFilter.Items.Add(texFilter);
            }

            Array texAddressValues = Enum.GetValues(typeof(TextureAddressMode));
            foreach (object texAddress in texAddressValues)
            {
                cbAddressU.Items.Add(texAddress);
                cbAddressV.Items.Add(texAddress);
                cbAddressW.Items.Add(texAddress);
            }

            cbSamplerIndex.HorizontalAlignment = HorizontalAlignment.Stretch;
            cbSamplerIndex.SelectionChanged += (sender, e) =>
            {
                SamplerStateBlock state = graphicsManager.GetSamplerStatePS(cbSamplerIndex.SelectedIndex);
                cbAddressU.SelectedIndex = (int)state.AddressU - 1;
                cbAddressV.SelectedIndex = (int)state.AddressV - 1;
                cbAddressW.SelectedIndex = (int)state.AddressW - 1;
                cbMagFilter.SelectedIndex = (int)state.MagFilter - ((int)state.MagFilter > 3 ? 2 : 0);
                cbMinFilter.SelectedIndex = (int)state.MinFilter - ((int)state.MinFilter > 3 ? 2 : 0);
                cbMipFilter.SelectedIndex = (int)state.MipFilter - ((int)state.MipFilter > 3 ? 2 : 0);

                Texture2D texture = graphicsManager.GetTexturePS(0);
                btnTexture.Content = (texture != null) ? texture.Name : "None";
            };

            int maxSamplers = graphicsManager.SamplerStateCount;
            for (int i = 0; i < maxSamplers; i++)
            {
                cbSamplerIndex.Items.Add(string.Format("register(ps, s{0})", i));

                if (i == cbSamplerIndex.SelectedIndex)
                {
                    SamplerStateBlock state = graphicsManager.GetSamplerStatePS(i);
                    cbAddressU.SelectedIndex = (int)state.AddressU - 1;
                    cbAddressV.SelectedIndex = (int)state.AddressV - 1;
                    cbAddressW.SelectedIndex = (int)state.AddressW - 1;
                    cbMagFilter.SelectedIndex = (int)state.MagFilter - ((int)state.MagFilter > 3 ? 2 : 0);
                    cbMinFilter.SelectedIndex = (int)state.MinFilter - ((int)state.MinFilter > 3 ? 2 : 0);
                    cbMipFilter.SelectedIndex = (int)state.MipFilter - ((int)state.MipFilter > 3 ? 2 : 0);
                }
            }
            cbSamplerIndex.SelectedIndex = 0;

            StackPanel stack = new StackPanel();
            stack.Orientation = Orientation.Vertical;
            stack.HorizontalAlignment = HorizontalAlignment.Stretch;

            StackPanel spTechnique = new StackPanel();
            spTechnique.Orientation = Orientation.Horizontal;
            spTechnique.HorizontalAlignment = HorizontalAlignment.Stretch;
            spTechnique.Children.Add(new Label() { Content = "Techique " });
            spTechnique.Children.Add(cbTechnique);
            stack.Children.Add(spTechnique);

            StackPanel spSamplerIndex = new StackPanel();
            spSamplerIndex.Orientation = Orientation.Horizontal;
            spSamplerIndex.HorizontalAlignment = HorizontalAlignment.Stretch;
            spSamplerIndex.Children.Add(new Label() { Content = "Sampler " });
            spSamplerIndex.Children.Add(cbSamplerIndex);
            stack.Children.Add(spSamplerIndex);

            StackPanel spMinFilter = new StackPanel();
            spMinFilter.Orientation = Orientation.Horizontal;
            spMinFilter.HorizontalAlignment = HorizontalAlignment.Stretch;
            spMinFilter.Children.Add(new Label() { Content = "Min Filter " });
            spMinFilter.Children.Add(cbMinFilter);
            stack.Children.Add(spMinFilter);

            StackPanel spMagFilter = new StackPanel();
            spMagFilter.Orientation = Orientation.Horizontal;
            spMagFilter.HorizontalAlignment = HorizontalAlignment.Stretch;
            spMagFilter.Children.Add(new Label() { Content = "Mag Filter " });
            spMagFilter.Children.Add(cbMagFilter);
            stack.Children.Add(spMagFilter);

            StackPanel spMipFilter = new StackPanel();
            spMipFilter.Orientation = Orientation.Horizontal;
            spMipFilter.HorizontalAlignment = HorizontalAlignment.Stretch;
            spMipFilter.Children.Add(new Label() { Content = "Mip Filter " });
            spMipFilter.Children.Add(cbMipFilter);
            stack.Children.Add(spMipFilter);

            StackPanel spAddressU = new StackPanel();
            spAddressU.Orientation = Orientation.Horizontal;
            spAddressU.HorizontalAlignment = HorizontalAlignment.Stretch;
            spAddressU.Children.Add(new Label() { Content = "Address U " });
            spAddressU.Children.Add(cbAddressU);
            stack.Children.Add(spAddressU);

            StackPanel spAddressV = new StackPanel();
            spAddressV.Orientation = Orientation.Horizontal;
            spAddressV.HorizontalAlignment = HorizontalAlignment.Stretch;
            spAddressV.Children.Add(new Label() { Content = "Address V " });
            spAddressV.Children.Add(cbAddressV);
            stack.Children.Add(spAddressV);

            StackPanel spAddressW = new StackPanel();
            spAddressW.Orientation = Orientation.Horizontal;
            spAddressW.HorizontalAlignment = HorizontalAlignment.Stretch;
            spAddressW.Children.Add(new Label() { Content = "Address W " });
            spAddressW.Children.Add(cbAddressW);
            stack.Children.Add(spAddressW);

            StackPanel spTexture = new StackPanel();
            spTexture.Orientation = Orientation.Horizontal;
            spTexture.HorizontalAlignment = HorizontalAlignment.Stretch;
            spTexture.Children.Add(new Label() { Content = "Texture " });
            spTexture.Children.Add(btnTexture);
            stack.Children.Add(spTexture);

            TreeViewItem treeViewSamplers = new TreeViewItem()
            {
                HorizontalAlignment = HorizontalAlignment.Stretch,
                Header = "Samplers"
            };
            treeViewSamplers.Items.Add(new TreeViewItem()
            {
                HorizontalAlignment = HorizontalAlignment.Stretch,
                Header = stack
            });
            treeViewRoot.Items.Add(treeViewSamplers);

            #endregion
        }

        #endregion

        #region Methods

        /// <summary>
        /// Adds a file to this instance as an included file.
        /// </summary>
        /// <param name="filename">Full path to the file being included.</param>
        /// <param name="hlslIncludePath">Path displayed in the effect code.</param>
        /// <returns>The new instance created for the include, note that it has already been
        /// added to the listing in this instance.</returns>
        public FxInfo AddInclude(string filename, string hlslIncludePath = null)
        {
            // ensure the include doesn't exist
            for (int i = 0; i < treeViewIncludes.Items.Count; i++)
            {
                TreeViewItem item = treeViewIncludes.Items[i] as TreeViewItem;
                if (item != null)
                {
                    string name = item.Header as string;
                    if (name != null &&
                        string.Equals(name, Path.GetFileName(filename), 
                                      StringComparison.CurrentCultureIgnoreCase))
                    {
                        return item.Tag as FxInfo;
                    }
                }
            }

            FxInfo fxInclude = new FxInfo(graphicsManager.Game, filename, this);
            fxInclude.hlslIncludePath = hlslIncludePath;
            fxIncludes.Add(fxInclude);

            ContextMenu ctxMenu = new ContextMenu();
            MenuItem ctxRemove = new MenuItem();
            ctxRemove.Header = "Remove";
            ctxRemove.Tag = fxInclude;
            ctxRemove.Click += new RoutedEventHandler(ctxRemoveInclude_Click);
            ctxMenu.Items.Add(ctxRemove);

            TreeViewItem tvi = new TreeViewItem();
            tvi.Header = Path.GetFileName(filename);
            tvi.Tag = fxInclude;
            tvi.ContextMenu = ctxMenu;
            tvi.MouseDoubleClick += (csender, ce) =>
            {
                fxInclude.Show(Manager);
                fxInclude.Activate();
            };
            treeViewIncludes.Items.Add(tvi);

            return fxInclude;
        }

        /// <summary>
        /// Saves this effect file and any of it's includes that have changed.
        /// </summary>
        public void SaveFiles(string filename = null, bool forced = false)
        {
            if (string.IsNullOrWhiteSpace(filename))
            {
                filename = sourceFilename;
                hasChangedSinceLastSave = true;
            }
            else if (!string.Equals(Path.GetFullPath(filename), sourceFilename,
                                    StringComparison.CurrentCultureIgnoreCase))
            {
                sourceFilename = Path.GetFullPath(filename);
                hasChangedSinceLastSave = true;
            }

            // if this is an include file, ensure relative paths are satisfied
            if (IsInclude && !Path.IsPathRooted(hlslIncludePath))
            {
                FileInfo fi = new FileInfo(fxParent.SourceFilename);
                string newPath = Path.Combine(fi.Directory.FullName, hlslIncludePath);
                if (!string.Equals(Path.GetFullPath(newPath), sourceFilename,
                                   StringComparison.CurrentCultureIgnoreCase))
                {
                    sourceFilename = newPath;
                    hasChangedSinceLastSave = true;
                }
            }

            // only save when a change is present or we're being forced to save
            if (hasChangedSinceLastSave || forced)
            {
                textEditor.Save(sourceFilename);
                hasChangedSinceLastSave = false;
                Title = Path.GetFileName(sourceFilename);
            }

            // save the include files now
            for (int i = 0; i < fxIncludes.Count; i++)
                fxIncludes[i].SaveFiles();
        }

        /// <summary>
        /// Attempts to put the text editor in focus for an error that occurred.
        /// </summary>
        /// <param name="error"></param>
        /// <returns>The instance the error occurred within.</returns>
        public FxInfo GoToErrorLocation(XEffectErrorOrWarning error)
        {
            int lineNumber, colNumber;
            if (!int.TryParse(error.Line, out lineNumber)) lineNumber = -1;
            if (!int.TryParse(error.Column, out colNumber)) colNumber = -1;

            int quoteIndex = error.Description.IndexOf('\'');
            if (quoteIndex > -1)
            {
                int endQuoteIndex = error.Description.IndexOf('\'', ++quoteIndex);
                int quoteLength = endQuoteIndex - quoteIndex;
                string quotedText = error.Description.Substring(quoteIndex, quoteLength);

                ManagedContent activeDocument = Manager.ActiveDocument;
                FxInfo mostLikelyMatch = FindMostLikelyMatch(activeDocument, lineNumber, colNumber, quotedText);
                if (mostLikelyMatch != null)
                {
                    int offset = mostLikelyMatch.textEditor.Document.GetOffset(lineNumber, Math.Max(colNumber, 0));
                    mostLikelyMatch.textEditor.Select(offset, quoteLength);
                    return mostLikelyMatch;
                }
            }

            return null;
        }
        FxInfo FindMostLikelyMatch(ManagedContent activeDocument, int line, int col, string quote)
        {
            FxInfo mostLikelyMatch = null;
            if (line <= textEditor.Document.LineCount)
            {
                int offset = textEditor.Document.GetOffset(line, Math.Max(col, 0));
                string lineText = textEditor.Document.GetText(offset, quote.Length);

                if (string.Equals(quote, lineText, StringComparison.InvariantCulture))
                {
                    mostLikelyMatch = this;
                    if (mostLikelyMatch == activeDocument)
                        return mostLikelyMatch;
                }
            }

            for (int i = 0; i < fxIncludes.Count; i++)
            {
                FxInfo result = fxIncludes[i].FindMostLikelyMatch(activeDocument, line, col, quote);
                if (result != null)
                {
                    mostLikelyMatch = result;
                    if (result == activeDocument)
                        break;
                }
            }
            return mostLikelyMatch;
        }

        /// <summary>
        /// Activates the preview renderer for this instance.
        /// </summary>
        public void ActivatePreview()
        {
            if (graphicsManager == null)
                return;

            bool alreadyShowing = false;
            for (int i = 0; i < graphicsManager.Game.Components.Count; i++)
            {
                if (graphicsManager.Game.Components[i] == graphicsManager)
                {
                    alreadyShowing = true;
                    continue;
                }

                if (graphicsManager.Game.Components[i] is GraphicsResourceManager)
                {
                    graphicsManager.Game.Components.RemoveAt(i);
                    break;
                }
            }

            if (!alreadyShowing)
                graphicsManager.Game.Components.Add(graphicsManager);
        }

        #endregion

        #region Compilation

        byte[] CompileEffect(bool debugBuild = true)
        {
            for (int i = 0; i < errorsAndWarnings.Count; i++)
            {
                RemoveError(errorsAndWarnings[i]);
            }
            errorsAndWarnings.Clear();

            ShaderBytecode result = null;
            ShaderFlags shaderFlags = ShaderFlags.None;
            EffectFlags effectFlags = EffectFlags.None;
            string shaderCode = textEditor.Document.Text;

            if (!debugBuild)
            {
                try
                {
                    string compileErrors;
                    result = SlimDX.D3DCompiler.ShaderBytecode.Compile(
                        shaderCode, "fx_2_0", shaderFlags, effectFlags, 
                        preprocessorMacros, includeHelper, 
                        out compileErrors);

                    if (!string.IsNullOrWhiteSpace(compileErrors))
                    {
                        ProcessErrorsAndWarnings(compileErrors);

                        // find if any of the reported problems is an error
                        bool isError = false;
                        for (int i = 0;
                             i < errorsAndWarnings.Count &&
                             (isError = !errorsAndWarnings[i].IsWarning);
                             i++) ;

                        // when there is an error in release builds, try recompiling as debug build
                        if (isError)
                        {
                            shaderFlags |= ShaderFlags.Debug | ShaderFlags.SkipOptimization;
                            debugBuild = true;
                            errorsAndWarnings.Clear();
                            result = null;
                        }
                    }
                }
                catch (CompilationException ex)
                {
                    ProcessErrorsAndWarnings(ex.Message);

                    // find if any of the reported problems is an error
                    bool isError = false;
                    for (int i = 0;
                         i < errorsAndWarnings.Count &&
                         (isError = !errorsAndWarnings[i].IsWarning);
                         i++) ;

                    // when there is an error in release builds, try recompiling as debug build
                    if (isError)
                    {
                        shaderFlags |= ShaderFlags.Debug | ShaderFlags.SkipOptimization;
                        debugBuild = true;
                        errorsAndWarnings.Clear();
                        result = null;
                    }
                }
            }

            if (debugBuild)
            {
                try
                {
                    string compileErrors;
                    result = SlimDX.D3DCompiler.ShaderBytecode.Compile(
                        shaderCode, "fx_2_0", shaderFlags, effectFlags,
                        preprocessorMacros, includeHelper,
                        out compileErrors);

                    if (!string.IsNullOrWhiteSpace(compileErrors))
                    {
                        ProcessErrorsAndWarnings(compileErrors);

                        // find if any of the reported problems is an error
                        bool isError = false;
                        for (int i = 0;
                             i < errorsAndWarnings.Count &&
                             (isError = !errorsAndWarnings[i].IsWarning);
                             i++) ;

                        // when there is an error in debug builds the byte code is irrelevant
                        if (isError)
                            result = null;
                    }
                }
                catch (CompilationException ex)
                {
                    ProcessErrorsAndWarnings(ex.Message);

                    // find if any of the reported problems is an error
                    bool isError = false;
                    for (int i = 0;
                         i < errorsAndWarnings.Count &&
                         (isError = !errorsAndWarnings[i].IsWarning);
                         i++) ;

                    // when there is an error in debug builds the byte code is irrelevant
                    if (isError)
                        result = null;
                }
            }

            for (int i = 0; i < errorsAndWarnings.Count; i++)
            {
                RaiseError(errorsAndWarnings[i]);
            }

            // if there is a compiled result at this point, send back the code from it
            if (result != null)
            {
                byte[] byteCode = new byte[result.Data.Length];
                result.Data.Read(byteCode, 0, byteCode.Length);
                return byteCode;
            }
            return null;
        }

        void ProcessErrorsAndWarnings(string errorsAndWarnings)
        {
            string root = Path.GetFileName(sourceFilename);
            string[] errors = errorsAndWarnings.Split('\n');
            for (int i = 0; i < errors.Length; i++)
            {
                if (errors[i].StartsWith(Environment.NewLine))
                    break;

                // find some unique characters in the error string
                int openIndex = errors[i].IndexOf('(');
                int closeIndex = errors[i].IndexOf(')');

                // can't process the message if it has no line counter
                if (openIndex == -1 || closeIndex == -1)
                    continue;

                // find the error number, then move forward into the message
                int errorIndex = errors[i].IndexOf('X', closeIndex);
                if (errorIndex < 0)
                {
                    this.errorsAndWarnings.Add(new XEffectErrorOrWarning()
                    {
                        Source = this,
                        Description = errors[i],
                        Root = root,
                        Type = "Error"
                    });
                    continue;
                }

                // trim out the data we need to feed the logger
                string filename = Path.GetFileName(errors[i].Remove(openIndex));
                string lineAndColumn = errors[i].Substring(openIndex + 1, closeIndex - openIndex - 1);
                string description = errors[i].Substring(errorIndex);
                string[] lc = lineAndColumn.Split(',');

                // construct the temporary content identity and file the error or warning
                if (errors[i].Contains("warning"))
                {
                    this.errorsAndWarnings.Add(new XEffectErrorOrWarning()
                    {
                        Source = this,
                        Description = errors[i],
                        Line = lc.Length > 0 ? lc[0] : string.Empty,
                        Column = lc.Length > 1 ? lc[1] : string.Empty,
                        File = filename,
                        Root = root,
                        IsWarning = true,
                        Type = "Warning"
                    });
                }
                else if (errors[i].Contains("error"))
                {
                    // handle the stupid non-conformant error messages
                    if (description.StartsWith("ID3DXEffectCompiler") ||
                        description.StartsWith("XEffectCompiler"))
                    {
                        errorIndex = errors[0].IndexOf('X');
                        if (errorIndex > 0)
                        {
                            int endOfErrorIndex = errors[0].IndexOf(';', errorIndex);
                            int errorLength = endOfErrorIndex - errorIndex;
                            if (errorLength > 0)
                                description = errors[0].Substring(errorIndex, errorLength);
                        }
                    }

                    this.errorsAndWarnings.Add(new XEffectErrorOrWarning()
                    {
                        Source = this,
                        Description = description,
                        Line = lc.Length > 0 ? lc[0] : string.Empty,
                        Column = lc.Length > 1 ? lc[1] : string.Empty,
                        File = filename,
                        Root = root,
                        IsWarning = false,
                        Type = "Error"
                    });
                }
            }
        }

        void RemoveInclude(TreeViewItem includeItem)
        {
            string code = textEditor.Document.Text;
            int lineNumber = 0;
            using (StringReader reader = new StringReader(code))
            {
                string line;
                while ((line = reader.ReadLine()) != null)
                {
                    lineNumber++;
                    int includeIndex = line.IndexOf("#include");
                    if (includeIndex > -1)
                    {
                        int quoteStart = line.IndexOf('"', includeIndex + 9);
                        if (quoteStart > -1)
                        {
                            int quoteEnd = line.IndexOf('"', ++quoteStart);
                            if (quoteEnd > -1)
                            {
                                int quoteLength = quoteEnd - quoteStart;
                                string path = line.Substring(quoteStart, quoteLength);
                                if (string.Equals(path,
                                                  (includeItem.Tag as FxInfo).hlslIncludePath,
                                                  StringComparison.InvariantCultureIgnoreCase))
                                {
                                    break;
                                }
                            }
                        }
                    }
                }
            }

            DocumentLine docLine = textEditor.Document.GetLineByNumber(lineNumber);
            textEditor.Document.Remove(docLine);
            treeViewIncludes.Items.Remove(includeItem);
        }

        #endregion

        #region Event Handlers

        void TextArea_TextEntered(object sender, TextCompositionEventArgs e)
        {
            if (!hasChangedSinceLastSave)
                Title += "*";
            hasChangedSinceLastSave = true;
        }
        void textEditor_TextChanged(object sender, EventArgs e)
        {
            Dispatcher.BeginInvoke(new Action<FoldingManager, TextDocument>(foldingStrategy.UpdateFoldings),
                                   foldingManager, textEditor.Document);

            if (!hasChangedSinceLastSave)
                Title += "*";
            hasChangedSinceLastSave = true;

            if (IsEffect)
            {
                byte[] byteCode = CompileEffect();
                if (byteCode != null)
                    graphicsManager.EffectCode = byteCode;
            }
        }

        void ctxIncludeAddNew_Click(object sender, RoutedEventArgs e)
        {
            FxInfo fxInclude = new FxInfo(graphicsManager.Game,
                                        string.Format("Include{0}.fxh", fxIncludes.Count),
                                        this);
            fxInclude.hlslIncludePath = Path.GetFileName(fxInclude.sourceFilename);
            fxInclude.Editor.TextChanged += (csender, ce) =>
            {
                byte[] byteCode = CompileEffect();
                if (byteCode != null)
                    graphicsManager.EffectCode = byteCode;
            };
            fxIncludes.Add(fxInclude);

            ContextMenu ctxMenu = new ContextMenu();
            MenuItem ctxRemove = new MenuItem();
            ctxRemove.Header = "Remove";
            ctxRemove.Tag = fxInclude;
            ctxRemove.Click += new RoutedEventHandler(ctxRemoveInclude_Click);
            ctxMenu.Items.Add(ctxRemove);

            TreeViewItem tvi = new TreeViewItem();
            tvi.Header = Path.GetFileName(fxInclude.sourceFilename);
            tvi.Tag = fxInclude;
            tvi.ContextMenu = ctxMenu;
            tvi.MouseDoubleClick += (csender, ce) =>
            {
                fxInclude.Show(Manager);
                fxInclude.Activate();
            };
            treeViewIncludes.Items.Add(tvi);

            // insert the new reference into the code
            string code = textEditor.Document.Text;
            int insertIndex = 0;
            code = code.Insert(insertIndex, string.Format("#include \"{0}\"{1}",
                                                          fxInclude.hlslIncludePath,
                                                          Environment.NewLine));
            textEditor.Document.Text = code;
        }
        void ctxIncludeAddExisting_Click(object sender, RoutedEventArgs e)
        {
            OpenFileDialog fileDialog = new OpenFileDialog();
            fileDialog.Filter = FxResources.Filter_FxIncludes;
            if (fileDialog.ShowDialog() ?? false)
            {
                FxInfo fxInclude = new FxInfo(graphicsManager.Game,
                                              fileDialog.FileName,
                                              this);
                fxInclude.hlslIncludePath = Path.GetFileName(fxInclude.sourceFilename);
                fxIncludes.Add(fxInclude);

                ContextMenu ctxMenu = new ContextMenu();
                MenuItem ctxRemove = new MenuItem();
                ctxRemove.Header = "Remove";
                ctxRemove.Tag = fxInclude;
                ctxRemove.Click += new RoutedEventHandler(ctxRemoveInclude_Click);
                ctxMenu.Items.Add(ctxRemove);

                TreeViewItem tvi = new TreeViewItem();
                tvi.Header = Path.GetFileName(fxInclude.sourceFilename);
                tvi.Tag = fxInclude;
                tvi.ContextMenu = ctxMenu;
                tvi.MouseDoubleClick += (csender, ce) =>
                {
                    fxInclude.Show(Manager);
                    fxInclude.Activate();
                };
                treeViewIncludes.Items.Add(tvi);

                // inser the new reference into the code
                string code = textEditor.Document.Text;
                int insertIndex = code.LastIndexOf("#include");
                if (insertIndex >= 0)
                    while (!code[++insertIndex].Equals(Environment.NewLine)) ;
                else
                    insertIndex = 0;
                code = code.Insert(insertIndex, string.Format("#include \"{0}\"{1}",
                                                              fxInclude.hlslIncludePath,
                                                              Environment.NewLine));
                textEditor.Document.Text = code;
            }
        }
        void ctxRemoveInclude_Click(object sender, RoutedEventArgs e)
        {
            TreeViewItem item = sender as TreeViewItem;
            if (item != null)
                RemoveInclude(item);
        }
        void ctxIncludeRemoveAll_Click(object sender, RoutedEventArgs e)
        {
            foreach (TreeViewItem item in treeViewIncludes.Items)
            {
                RemoveInclude(item);
            }
        }
        void ctxClose_Click(object sender, RoutedEventArgs e)
        {
            if (hasChangedSinceLastSave)
            {
                MessageBoxResult result = MessageBox.Show(
                    "Would you like to save pending changes to file?",
                    "Warning", MessageBoxButton.YesNoCancel);

                if (result == MessageBoxResult.Yes)
                {
                    Closing(this);
                    SaveFiles(sourceFilename);
                }
                else if (result == MessageBoxResult.No)
                {
                    Closing(this);
                    Close();
                }
                else
                    return;
            }
            else
            {
                Closing(this);
                Close();
            }
        }

        void graphicsManager_EffectCreated(Microsoft.Xna.Framework.Graphics.Effect effect)
        {
            string previousTechnique = graphicsManager.ActiveTechniqueName;
            string[] techniques = new string[effect.Techniques.Count];
            int techniqueIndex = 0;
            for (int i = 0; i < effect.Techniques.Count; i++)
            {
                techniques[i] = effect.Techniques[i].Name;
                if (techniques[i] == previousTechnique)
                    techniqueIndex = i;
            }
            graphicsManager.ActiveTechniqueName = techniques[techniqueIndex];

            Dispatcher.BeginInvoke(new Action(() =>
            {
                cbTechnique.Items.Clear();
                for (int i = 0; i < techniques.Length; i++)
                    cbTechnique.Items.Add(techniques[i]);
                cbTechnique.SelectedIndex = techniqueIndex;
            }), null);
        }

        public override bool Close()
        {
            if (base.Close())
            {
                if (graphicsManager != null)
                    graphicsManager.Game.Components.Remove(graphicsManager);

                for (int i = 0; i < errorsAndWarnings.Count; i++)
                {
                    RemoveError(errorsAndWarnings[i]);
                }

                return true;
            }
            return false;
        }

        #endregion
    }
}
